package cn.com.zero.api.auth.util;

import cn.com.zero.api.auth.config.properties.TokenProperties;
import cn.com.zero.api.auth.constant.TokenConstants;
import cn.com.zero.base.constant.ZeroConstants;
import cn.com.zero.base.exception.ErrorCodeException;
import cn.com.zero.base.util.RandomUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import sun.misc.BASE64Decoder;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author xiongxj
 * @version 1.0.0
 * @Description token令牌工具类。通过使用JWT（json web token），生成的token中包含三部分信息：
 * (1)头部（header）：指定了其Token类型和所使用的加密算法
 * (2)载荷（payload）：存储的自定义数据。比如：用户信息、过期时间等。
 * (3)签名（signature）：对头部和载荷进行加密签名。
 * @createTime 2023/3/16 15:39
 */
public class TokenUtil {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    private TokenProperties tokenProperties;

    /**
     * 构建token工具类
     *
     * @param tokenProperties token配置信息
     */
    public TokenUtil(TokenProperties tokenProperties) {
        this.tokenProperties = tokenProperties;
    }

    /**
     * 生成 token
     *
     * @param tokenData token中需要携带的数据
     * @param issueTime 签发时间
     * @return token串
     */
    public <T> String generateToken(T tokenData, Date issueTime) {
        Assert.notNull(tokenData, "token携带数据[tokenData]不允许为空");
        Assert.notNull(issueTime, "签发时间[issueTime]不允许为空");
        Integer refreshTimes = 0;
        if (tokenProperties.getRefreshTimes() != null && tokenProperties.getRefreshTimes() > 0) {
            refreshTimes = tokenProperties.getRefreshTimes();
        }
        return generateToken(tokenData, issueTime, refreshTimes);
    }

    /**
     * 校验并刷新token
     *
     * @param token            待校验的token
     * @param currentIssueTime 当前签发时间
     * @param lastIssueTime    上次签发时间
     * @param tokenDataClazz   token携带数据的类型
     * @return 可使用的token，可能是原来的token，也可能是刷新后的token
     * @throws ErrorCodeException 发生返回码异常，表明校验未通过
     */
    public <T> String checkAndRefreshToken(String token, Date currentIssueTime, Date lastIssueTime, Class<T> tokenDataClazz) {
        // 校验token合法性
        Long checkResult = checkToken(token, lastIssueTime, tokenDataClazz);
        if (ZeroConstants.RETURN_CODE_SUCCESS.compareTo(checkResult) != 0) {
            throw new ErrorCodeException(TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE);
        }
        Claims claims = parseToken(token);
        if (null == claims) {
            throw new ErrorCodeException(TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE);
        }
        // 刷新次数
        Integer refreshTimes = claims.get(TokenConstants.CLAIMS_REFRESH_TIMES, Integer.class);
        if (null == refreshTimes || refreshTimes.compareTo(0) <= 0) {
            logger.debug("token不允许刷新");
            return token;
        }
        // 获取上次签发时间 若当前时间-最后一次生成token时间>=刷新间隔，则需要重新生成token
        Date issueTime = claims.getIssuedAt();
        Date currentTime = new Date();
        if (currentTime.getTime() - issueTime.getTime() >= TimeUnit.SECONDS.toMillis(tokenProperties.getRefreshInterval())) {
            T tokenData = JSON.parseObject(claims.getSubject(), tokenDataClazz);
            return generateToken(tokenData, currentIssueTime, refreshTimes - 1);
        }
        // 其他情况则返回原来的token
        return token;
    }

    /**
     * 获取 token中存储的主题内容
     *
     * @param token        token串
     * @param subjectClazz 主题内容的所属类类型
     * @return token中存储的主题内容
     */
    public <T> T getTokenSubject(String token, Class<T> subjectClazz) {
        Claims claims = parseToken(token);
        if (claims != null) {
            String subject = claims.getSubject();
            if (StrUtil.isEmpty(subject)) {
                return null;
            }
            return JSONObject.parseObject(subject, subjectClazz);
        }
        return null;
    }

    /**
     * 生成 RSA 密钥(非对称加密)
     *
     * @param keysize 密钥长度
     * @return 密钥(包含公钥和私钥)
     */
    public KeyPair generateRSAKey(int keysize) {
        try {
            // 获取指定算法的密钥对生成器
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
            // 初始化密钥对生成器（指定密钥长度, 使用默认的安全随机数源）
            generator.initialize(keysize);
            // 随机生成一对密钥（包含公钥和私钥）
            return generator.generateKeyPair();
        } catch (Exception e) {
            logger.warn("生成 RSA 密钥(非对称加密) 发生异常", e);
        }
        return null;
    }

    /**
     * 生成 ECDH 密钥(主要用于生成公私钥对使用)
     *
     * @return 密钥(包含公钥和私钥)
     */
    public KeyPair generateECDHKeyPair() {
        try {
            Security.addProvider(new BouncyCastleProvider());
            // 获取指定算法的密钥对生成器
            KeyPairGenerator generator = KeyPairGenerator.getInstance("ECDH", Security.getProvider("BC"));
            // 初始化密钥对生成器（指定密钥长度, 使用默认的安全随机数源）
            ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");//曲线secp256r1
            // 随机生成一对密钥（包含公钥和私钥）
            generator.initialize(ecSpec);
            return generator.generateKeyPair();
        } catch (Exception e) {
            logger.warn("生成 ECDH 密钥(主要用于生成公私钥对使用) 发生异常", e);
        }
        return null;
    }

    /**
     * 检验token是否合法
     *
     * @param token          token
     * @param issueTime      token签发时间
     * @param tokenDataClazz token携带数据的类型
     * @return true：token验证通过；false：token验证失败
     */
    private <T> Long checkToken(String token, Date issueTime, Class<T> tokenDataClazz) {
        Assert.notNull(token, "token不允许为空");
        Assert.notNull(issueTime, "token签发时间不允许为空");
        Assert.notNull(tokenDataClazz, "token携带的数据类型不允许为空");
        Claims claims = parseToken(token);
        if (null == claims) {
            logger.debug("校验token[{}]失败，原因：未获取到token携带的所有数据", token);
            return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
        }

        try {
            // 解析 token携带的数据，即主题subject
            try {
                JSON.parseObject(claims.getSubject(), tokenDataClazz);
            } catch (Exception e) {
                logger.debug("校验token[{}]失败，原因：无法成功解析token携带数据", token);
                return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
            }

            // 解析 token签发人
            String issuer = claims.getIssuer();
            if (!tokenProperties.getIssuer().equals(issuer)) {
                logger.debug("校验token[{}]失败，原因：token签发人[{{}]不匹配", token, issuer);
                return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
            }

            // 解析 token签发时间
            Date issuedAt = claims.getIssuedAt();
            // 对issueTime进行处理，去掉毫秒数，因为jwt获取的时间中不包含毫秒，若不处理，会验签不过
            Date issueTimeNoMills = new Date(issueTime.getTime() / 1000L * 1000L);
            if (issueTimeNoMills.compareTo(issuedAt) != 0) {
                logger.debug("校验token[{}]失败，原因：token签发时间[{{}]不匹配", token, DateUtil.formatDateTime(issuedAt));
                return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
            }

            // 解析 token过期时间
            Date expiration = claims.getExpiration();
            if (new Date().compareTo(expiration) >= 0) {
                logger.debug("校验token[{}]失败，原因：token已过期", token);
                return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
            }

            // 解析 刷新token次数
            Integer refreshTimes = claims.get(TokenConstants.CLAIMS_REFRESH_TIMES, Integer.class);
            Integer configRefreshTimes = tokenProperties.getRefreshTimes() == null ? 0 : tokenProperties.getRefreshTimes();
            if (null == refreshTimes || refreshTimes.compareTo(configRefreshTimes) > 0) {
                logger.debug("校验token[{}]失败，原因：token刷新次数[{{}]不匹配", token, refreshTimes);
                return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
            }

            return ZeroConstants.RETURN_CODE_SUCCESS;
        } catch (Exception e) {
            logger.debug("校验token失败，异常信息：", e);
            return TokenConstants.TOKEN_CHECK_FAILED_RETURN_CODE;
        }
    }

    /**
     * 生成 token
     *
     * @param tokenData    token中需要携带的数据
     * @param issueTime    签发时间
     * @param refreshTimes 刷新次数
     * @return token串
     */
    private <T> String generateToken(T tokenData, Date issueTime, Integer refreshTimes) {
        Assert.notNull(tokenData, "token携带数据[tokenData]不允许为空");
        Assert.notNull(issueTime, "签发时间[issueTime]不允许为空");
        Assert.notNull(refreshTimes, "刷新次数[refreshTimes]不允许为空");
        Assert.isTrue(refreshTimes.compareTo(0) >= 0, "刷新次数[refreshTimes]不允许小于0");
        JwtBuilder builder = builder(tokenData, issueTime, refreshTimes);
        return builder.compact();
    }

    /**
     * 获取生成jwt的builder
     *
     * @param tokenData    token中需要携带的数据
     * @param issueTime    签发时间
     * @param refreshTimes token刷新次数
     * @return 生成jwt的builder
     */
    private <T> JwtBuilder builder(T tokenData, Date issueTime, Integer refreshTimes) {
        JwtBuilder builder = Jwts.builder();

        // 组建token中需要携带的额外数据  token的刷新次数和最后一次生成token的时间
        Map<String, Object> additionalClaims = new HashMap<>();
        additionalClaims.put(TokenConstants.CLAIMS_REFRESH_TIMES, refreshTimes);

        // 转换成date类型的过期时间
        Date expire = new Date(issueTime.getTime() + TimeUnit.SECONDS.toMillis(tokenProperties.getExpireTime()));

        // 加密算法及secretKey
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(tokenProperties.getSignatureAlgorithm());

        // 注意使用了Claims，则不可再使用setPayload，因为Claims与Payload是冲突的，只能择其一。
        // 设置token的唯一id
        builder.setId(RandomUtil.getRandomStr())
                // 设置主题，也就是设置token中携带的数据，实际上也是通过key-value形式存储到claims的map中的，此处的key为sub，value为dataJsonStr
                .setSubject(JSON.toJSONString(tokenData))
                // 设置签发人
                .setIssuer(tokenProperties.getIssuer())
                // 设置签发时间 表示首次签发
                .setIssuedAt(issueTime)
                // 设置过期时间
                .setExpiration(expire)
                // 设置token中需要携带的额外数据
                .addClaims(additionalClaims);

        // 设置签名
        switch (signatureAlgorithm.getFamilyName()) {
            case "HMAC": {
                builder.signWith(signatureAlgorithm, key());
                break;
            }
            case "RSA": {
                builder.signWith(signatureAlgorithm, getRSAPrivateKey(tokenProperties.getEncodeKey()));
                break;
            }

            case "Elliptic Curve": {
                builder.signWith(signatureAlgorithm, getECPrivateKey(tokenProperties.getEncodeKey()));
                break;
            }
            default: {
                break;
            }
        }

        return builder;
    }

    /**
     * 解析token
     *
     * @param token token
     * @return 解析结果集，即token中携带的所有数据
     */
    private Claims parseToken(String token) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.forName(tokenProperties.getSignatureAlgorithm());
        switch (signatureAlgorithm.getFamilyName()) {
            case "HMAC":
                return parseToken(token, null);
            case "RSA":
                return parseToken(token, getRSAPublicKey(tokenProperties.getDecodeKey()));
            case "Elliptic Curve":
                return parseToken(token, getECPublicKey(tokenProperties.getDecodeKey()));
            default:
                return null;
        }
    }

    /**
     * 解析token(对称加密)
     *
     * @param token token
     * @return 解析结果集，即token中携带的所有数据
     */
    private Claims parseToken(String token, PublicKey publicKey) {
        // 获取解析器
        JwtParser parser = Jwts.parser();
        if (null == publicKey) {
            // 说明是对称加密，必须与加密时的密钥一致才能解析成功
            parser.setSigningKey(key());
        } else {
            // 设置解密的公钥，必须与加密时的私钥是一对才能解密成功
            parser.setSigningKey(publicKey);
        }

        try {
            Jws<Claims> claimsJws = parser.parseClaimsJws(token);
            return claimsJws.getBody();
        } catch (Exception e) {
            logger.debug("解析token[{}]失败，异常信息：", token, e);
            return null;
        }
    }

    /**
     * 用于加密的密钥(对称加密)
     *
     * @return 密钥
     */
    private SecretKey key() {
        byte[] encodedKey = Base64.getDecoder().decode(tokenProperties.getEncodeKey());
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

    /**
     * 获取PublicKey对象(非对称加密)
     *
     * @param key 公钥字符串(非对称加密)
     * @return 公钥对象(非对称加密)
     */
    private PublicKey getRSAPublicKey(String key) {
        try {
            key = key.replaceAll("\\-*BEGIN.*KEY\\-*", "").replaceAll("\\-*END.*KEY\\-*", "")
                    .replaceAll("\r", "").replaceAll("\n", "");
            byte[] keyBytes;
            keyBytes = (new BASE64Decoder()).decodeBuffer(key);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取PrivateKey对象(非对称加密)
     *
     * @param key 私钥字符串(非对称加密)
     * @return 私钥对象(非对称加密)
     */
    private PrivateKey getRSAPrivateKey(String key) {
        try {
            key = key.replaceAll("\\-*BEGIN.*KEY\\-*", "").replaceAll("\\-*END.*KEY\\-*", "")
                    .replaceAll("\r", "").replaceAll("\n", "");
            byte[] keyBytes;
            keyBytes = (new BASE64Decoder()).decodeBuffer(key);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
            return privateKey;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取PrivateKey对象(ECDSA算法)
     *
     * @param key 私钥字符串(ECDSA算法)
     * @return 私钥对象(ECDSA算法)
     */
    private PrivateKey getECPrivateKey(String key) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("ECDH", "BC");
            key = key.replaceAll("\\-*BEGIN.*KEY\\-*", "").replaceAll("\\-*END.*KEY\\-*", "")
                    .replaceAll("\r", "").replaceAll("\n", "");
            byte[] keyBytes = (new BASE64Decoder()).decodeBuffer(key);
            PKCS8EncodedKeySpec devicePriKeySpec = new PKCS8EncodedKeySpec(keyBytes);
            return keyFactory.generatePrivate(devicePriKeySpec);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取PublicKey对象(ECDSA算法)
     *
     * @param key 公钥字符串(ECDSA算法)
     * @return 公钥对象(ECDSA算法)
     */
    private PublicKey getECPublicKey(String key) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("ECDH", "BC");
            key = key.replaceAll("\\-*BEGIN.*KEY\\-*", "").replaceAll("\\-*END.*KEY\\-*", "")
                    .replaceAll("\r", "").replaceAll("\n", "");
            byte[] keyBytes = (new BASE64Decoder()).decodeBuffer(key);
            X509EncodedKeySpec pubX509 = new X509EncodedKeySpec(keyBytes);
            return keyFactory.generatePublic(pubX509);
        } catch (Exception e) {
            return null;
        }
    }

    public TokenProperties getTokenProperties() {
        return tokenProperties;
    }
}
